Desbalanceamento de Classes: Um Problema Real em Ciência de Dados
Hoje vamos conversar sobre um desafio comum e muitas vezes subestimado em projetos de ciência de dados e aprendizagem de máquina: o desbalanceamento de classes. Esse problema ocorre quando as classes de interesse em um conjunto de dados não são representadas igualmente. Por exemplo, em um problema de detecção de fraude, a maioria das transações será legítima (classe majoritária), enquanto pouquíssimas serão fraudulentas (classe minoritária).
Quando o desbalanceamento se torna um problema?
Não existe uma regra universal para definir a partir de qual proporção um conjunto de dados é “desbalanceado”. A verdade é que a definição de desbalanceamento é contextual e depende do problema que você está resolvendo.
No entanto, para efeitos práticos, se a sua classe de interesse (geralmente a minoritária) representa menos de 40% do seu conjunto de dados, já é um indício de que a base de dados é desbalanceada e podemos pensar em estratégias para lidar com isso. Quanto menor essa proporção, mais crítico se torna o problema. Cenários com 1:100 (1% da classe minoritária), como detecção de fraudes ou doenças raras, são considerados severamente desbalanceados e exigem tratamento específico. Proporções ainda mais extremas, como 1:1000 ou menos, são comuns em domínios como a detecção de ataques cibernéticos.
A tabela a seguir apresenta os nomes e as faixas geralmente aceitas para diferentes graus de desbalanceamento de classes:
| Porcentagem de dados pertencentes à classe minoritária | Grau de desequilíbrio |
|---|---|
| 20%-40% do conjunto de dados | Leve |
| 1%-20% do conjunto de dados | Moderado |
| <1% do conjunto de dados | Extremo |
Consequências do desbalanceamento de classes
O desbalanceamento de classes pode ter um impacto significativo na performance dos seus modelos, levando a conclusões enganosas. Aqui estão as principais consequências:
Viés para a Classe Majoritária: Modelos de aprendizagem de máquina tendem a ser otimizados para a acurácia geral. Quando há um desbalanceamento, eles podem aprender a prever a classe majoritária com alta frequência para minimizar o erro total. Isso resulta em uma acurácia inflacionada, que não reflete a capacidade real do modelo de identificar a classe minoritária, geralmente a de maior interesse (ex: fraudes, doenças raras).
Baixo Desempenho na Classe Minoritária: A consequência direta do viés é que o modelo terá dificuldade em identificar corretamente as instâncias da classe minoritária. Isso se traduz em métricas como precisão, recall e F1-score muito baixas para essa classe, indicando que o modelo é ineficaz para o objetivo principal.
Modelos Inúteis na Prática: Um modelo que não consegue identificar a classe minoritária, mesmo com uma alta acurácia geral, é frequentemente inútil em cenários reais. Em muitos problemas, a classe minoritária é a que carrega o maior valor ou risco (fraudes, doenças, defeitos). Mesmo um desbalanceamento “moderado” pode significar a perda de oportunidades cruciais ou a falha na detecção de eventos importantes.
Como lidar com o desbalanceamento?
Lidar com o desbalanceamento de classes é um passo crucial para construir modelos preditivos eficazes, especialmente quando a classe minoritária é a de maior interesse. Felizmente, existem diversas estratégias que podemos empregar para mitigar esse problema. Vamos explorar as principais, acompanhadas de exemplos práticos em R.
1. Reamostragem (Resampling)
A reamostragem é uma das abordagens mais diretas e populares, focando em alterar a proporção das classes no seu conjunto de dados de treinamento.
Existem duas vertentes principais, que também podem trabalhar simultaneamente:
- Oversampling (Sobreamostragem): A ideia do oversampling é aumentar o número de instâncias da classe minoritária, tornando-a mais “visível” para o algoritmo de aprendizado. Uma técnica amplamente utilizada é o SMOTE (Synthetic Minority Over-sampling Technique). O SMOTE não duplica as amostras existentes; ele cria novos exemplos sintéticos da classe minoritária, baseando-se nas similaridades entre os exemplos minoritários existentes (seus vizinhos mais próximos). Isso ajuda a expandir a representação da classe minoritária sem introduzir overfitting por repetição exata de dados.
# Exemplo de SMOTE em R
# Primeiramente, instale o pacote 'smotefamily' se ainda não tiver:
# install.packages("smotefamily")
library(smotefamily)
library(tidyverse)
library(ROSE)
data(hacide)
# Gerar o gráfico de barras
ggplot(dados_treino, aes(x = target, fill = target)) +
geom_bar() +
# Adicionar rótulos de contagem em cima das barras
geom_text(stat = 'count', aes(label = after_stat(count)), vjust = -0.5) +
labs(
title = "Distribuição das Classes em Dados Sintéticos Desbalanceados",
x = "Classe",
y = "Contagem de Amostras",
fill = "Classe"
) +
scale_fill_manual(values = c("1" = "#FD800D", "0" = "#2077B5")) + # Cores personalizadas
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5)) # Centraliza o título# Preparando os dados para smotefamily::SMOTE()
# A função SMOTE() do smotefamily espera:
# - X: um dataframe de variáveis preditoras (apenas numéricas)
# - target: um vetor da variável target
# - K: número de vizinhos (padrão é 5)
# - dup_size: um fator de duplicação para a classe minoritária.
# Se a minoritária é 100 e queremos que ela tenha 300 (3x), dup_size seria 2 (100 + 2*100 = 300)
# Separando as features (X) da variável target
X_features <- dados_treino %>% dplyr::select(-target)
y_target <- dados_treino$target
# Aplicando SMOTE usando smotefamily
# Queremos que a classe '1' (minoritária) tenha aproximadamente 33 vezes o número original de exemplos.
# O número original é 43. Queremos aproximadamente 1450.
# O fator de duplicação (dup_size) é o número de cópias sintéticas *além* das originais.
# Então, para ir de 43 para 1450, precisamos de aproximadamente 1400 novas amostras, o que é 33x o original.
# Se target='1' tem 43 amostras, e você quer 'dup_size=33', você terá 43 (originais) + 33*43 (sintéticas) = 1462 amostras '1'.
dados_smote <- SMOTE(X = X_features, target = y_target, K = 5, dup_size = 33)
# O resultado vem como uma lista, precisamos extrair o dataframe balanceado
dados_smote_df <- dados_smote$data
# Gerar o gráfico de barras
ggplot(dados_smote_df, aes(x = class, fill = class)) +
geom_bar() +
# Adicionar rótulos de contagem em cima das barras
geom_text(stat = 'count', aes(label = after_stat(count)), vjust = -0.5) +
labs(
title = "Distribuição das classes após SMOTE",
x = "Classe",
y = "Contagem de Amostras",
fill = "Classe"
) +
scale_fill_manual(values = c("1" = "#FD800D", "0" = "#2077B5")) + # Cores personalizadas
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5)) # Centraliza o título- Undersampling (Subamostragem): Em contraste com o oversampling, o undersampling visa reduzir o número de instâncias da classe majoritária para equilibrar as proporções. A forma mais simples é a subamostragem aleatória, onde instâncias da classe majoritária são removidas aleatoriamente até que o balanço desejado seja atingido. Embora possa ser eficaz, a principal desvantagem é a potencial perda de informações importantes contidas nas amostras descartadas da classe majoritária. Por isso, deve ser usado com cautela, especialmente em datasets menores.
# Exemplo de Undersampling aleatório em R
# Instale o pacote 'ROSE' se ainda não tiver:
# install.packages("ROSE")
library(ROSE)
# Usando os mesmos 'dados' desbalanceados criados anteriormente
# Aplicando Undersampling: o método "under" do ovun.balance tentará equilibrar as classes
# reduzindo a majoritária para o mesmo número de exemplos da minoritária.
dados_under <- ovun.sample(target ~ ., data = dados_treino, method = "under")$data
# Gerar o gráfico de barras
ggplot(dados_under, aes(x = target, fill = target)) +
geom_bar() +
# Adicionar rótulos de contagem em cima das barras
geom_text(stat = 'count', aes(label = after_stat(count)), vjust = -0.5) +
labs(
title = "Distribuição das classes após undersampling",
x = "Classe",
y = "Contagem de Amostras",
fill = "Classe"
) +
scale_fill_manual(values = c("1" = "#FD800D", "0" = "#2077B5")) + # Cores personalizadas
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5)) # Centraliza o título- Oversampling e undersampling combinados: A combinação de oversampling e undersampling é frequentemente a estratégia mais eficaz, especialmente em casos de desbalanceamento severo. A ideia é aumentar a classe minoritária (com SMOTE, por exemplo) e, ao mesmo tempo, reduzir a classe majoritária, mas sem perder informações cruciais. Isso permite um controle mais fino sobre a proporção final das classes e pode resultar em modelos mais robustos.
# Exemplo de Oversampling e Undersampling simultâneos com ROSE
# Instale o pacote 'ROSE' se ainda não tiver:
# install.packages("ROSE")
library(ROSE)
# Usando os mesmos 'dados' desbalanceados criados anteriormente
# Aplicando reamostragem híbrida: tentando balancear as classes
# 'method = "both"' usa uma combinação de oversampling e undersampling.
dados_hibrido <- ovun.sample(target ~ ., data = dados_treino, method = "both")$data
# Gerar o gráfico de barras
ggplot(dados_hibrido, aes(x = target, fill = target)) +
geom_bar() +
# Adicionar rótulos de contagem em cima das barras
geom_text(stat = 'count', aes(label = after_stat(count)), vjust = -0.5) +
labs(
title = "Distribuição das classes após combinação de over e undersampling",
x = "Classe",
y = "Contagem de Amostras",
fill = "Classe"
) +
scale_fill_manual(values = c("1" = "#FD800D", "0" = "#2077B5")) + # Cores personalizadas
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5)) # Centraliza o título2. Mudança no Algoritmo: Ajustes Intrínsecos para o Desbalanceamento
Alguns algoritmos de aprendizado de máquina oferecem mecanismos internos ou parâmetros que podem ser ajustados para lidar com o desbalanceamento, sem a necessidade de reamostrar o conjunto de dados diretamente. Esses algoritmos permitem atribuir “custos” diferentes a erros de classificação. Por exemplo, em um problema de detecção de doenças raras, um falso negativo (não detectar a doença quando ela existe) pode ter um custo muito maior (ex: vida humana) do que um falso positivo (diagnosticar a doença quando ela não existe). Ao definir uma matriz de custos, o algoritmo será penalizado mais severamente por cometer erros na classe minoritária, incentivando-o a classificá-la corretamente.
- Algoritmos de Cost-Sensitive Learning:
- Árvores de Decisão
- Máquinas de Vetor de Suporte
- Boosting Algorithms
- Redes Neurais Artificiais
- Regressão Logística
Exemplo Prático: Random Forest com pesos de classe
Vamos primeiro treinar um modelo de Random Forest padrão para ver como ele se comporta sem nenhuma consideração de custo.
library(randomForest)
library(caret)
library(pROC)
# Treinar o modelo Random Forest
# ntree: número de árvores na floresta
# importance: calcula a importância das variáveis (opcional, mas útil)
modelo_rf_base <- randomForest(target ~ ., data = dados_treino, ntree = 500, classwt = NULL, importance = TRUE)
# Fazer previsões no conjunto de teste
# type="prob" para obter as probabilidades das classes
predicoes_rf_base <- predict(modelo_rf_base, newdata = dados_teste, type = "class")
# Avaliar o modelo
mc_rf_base <- confusionMatrix(predicoes_rf_base, dados_teste$target, positive = "1")
mc_rf_baseConfusion Matrix and Statistics
Reference
Prediction 0 1
0 482 17
1 1 0
Accuracy : 0.964
95% CI : (0.9437, 0.9785)
No Information Rate : 0.966
P-Value [Acc > NIR] : 0.656554
Kappa : -0.0038
Mcnemar's Test P-Value : 0.000407
Sensitivity : 0.0000
Specificity : 0.9979
Pos Pred Value : 0.0000
Neg Pred Value : 0.9659
Prevalence : 0.0340
Detection Rate : 0.0000
Detection Prevalence : 0.0020
Balanced Accuracy : 0.4990
'Positive' Class : 1
Note que temos uma acurácia geral alta, mas um Recall (Sensitivity) baixo para a classe “p” (minoritária), indicando que o modelo tem dificuldade em detectá-la. Agora, vamos treinar outro modelo Random Forest, mas desta vez, aplicando pesos de classe para dar mais importância à classe minoritária.
No nosso banco de dados de treinamento,
freq_classes <- table(dados_treino$target)
freq_classes
0 1
1457 43
a proporção é 1457 indivíduos na “0” e 43 na classe “1”, a classe “0” tem aproximadamente 34 vezes mais amostras. Para dar mais peso à classe “1”, poderíamos usar:
Pesos 0 = 1
Pesos 1 = (Número de amostras '0') / (Número de amostras '1')
Ou, mais genericamente, a frequência inversa:
Pesos 0 = 1 / Frequência de 0
Pesos 1 = 1 / Frequência de 1
# Definir os pesos de classe
# Dar um peso maior à classe '1' (minoritária)
pesos_classes <- 1 / freq_classes
pesos_classes <- pesos_classes / min(pesos_classes) # Normaliza para que o menor peso seja 1
# Treinar o modelo Random Forest com pesos de classe
modelo_rf_custo <- randomForest(target ~ .,
data = dados_treino,
ntree = 500,
classwt = pesos_classes, # AQUI aplicamos os pesos de classe!
importance = TRUE)
# Fazer previsões no conjunto de teste
predicoes_rf_custo <- predict(modelo_rf_custo, newdata = dados_teste, type = "class")
# Avaliar o modelo
mc_rf_custo <- confusionMatrix(predicoes_rf_custo, dados_teste$target, positive = "1")
mc_rf_custoConfusion Matrix and Statistics
Reference
Prediction 0 1
0 483 17
1 0 0
Accuracy : 0.966
95% CI : (0.9461, 0.9801)
No Information Rate : 0.966
P-Value [Acc > NIR] : 0.5640325
Kappa : 0
Mcnemar's Test P-Value : 0.0001042
Sensitivity : 0.000
Specificity : 1.000
Pos Pred Value : NaN
Neg Pred Value : 0.966
Prevalence : 0.034
Detection Rate : 0.000
Detection Prevalence : 0.000
Balanced Accuracy : 0.500
'Positive' Class : 1
3. Mudança nas métricas de avaliação
Como discutido anteriormente, a acurácia é uma métrica extremamente enganosa em conjuntos de dados desbalanceados. É absolutamente essencial mudar o foco para métricas que forneçam uma imagem mais fiel do desempenho do seu modelo, especialmente na classe minoritária.
Precisão (Precision): Responde à pergunta: “Das vezes que meu modelo previu a classe positiva, quantas estavam realmente corretas?”.Recall (Sensibilidade/Revocação): Responde à pergunta: “Das vezes que a classe positiva realmente ocorreu, quantas meu modelo conseguiu detectar?”. Para a classe minoritária, orecallé frequentemente a métrica mais crítica, pois indica a capacidade do modelo de “encontrar” as instâncias raras.F1-Score: É a média harmônica daPrecisãoe doRecall. É uma métrica útil quando você precisa de um equilíbrio entre não ter muitos falsos positivos (precisão) e não perder muitos positivos reais (recall).Curvas
ROC (Receiver Operating Characteristic)ePR (Precision-Recall):- A
AUC-ROCavalia a capacidade do modelo de distinguir entre as classes em diferentes limiares de decisão. - A
AUC-PRé frequentemente preferível àAUC-ROCpara conjuntos de dados altamente desbalanceados. Ela se concentra especificamente na relação entre Precisão e Recall, que são métricas mais informativas sobre o desempenho da classe minoritária quando há um grande desequilíbrio. Uma altaAUC-PRindica um bom desempenho na identificação da classe positiva sem um excesso de falsos positivos.
- A
Conclusão
Lidar com o desbalanceamento de classes é mais do que apenas uma tarefa técnica; é uma questão de garantir que seus modelos sejam úteis e confiáveis no mundo real, especialmente para os eventos raros, mas críticos. A chave é identificar o problema cedo (lembre-se: se a classe minoritária está abaixo de 40%, já é um alerta!), compreender suas consequências e aplicar as técnicas mais adequadas de reamostragem (incluindo a abordagem híbrida), ajuste de algoritmo ou avaliação de métricas.
Não existe uma solução única que sirva para todos os casos. A escolha da melhor abordagem depende do seu conjunto de dados, do algoritmo que você está usando e, fundamentalmente, do objetivo de negócio. A experimentação e a validação cuidadosa são sempre os melhores caminhos!